ES6 Module 模块化面试题全解析
一、核心要点速览
💡 核心考点
- export/import: ES6 模块化语法
- default export: 默认导出
- named export: 具名导出
- vs CommonJS: 值的引用 vs 值的拷贝
- tree-shaking: 移除未使用代码
二、export 与 import 基础
1. 导出方式
javascript
// ========== 具名导出(Named Export)==========
// 方式 1:声明时导出
export const PI = 3.14159
export function add(a, b) {
return a + b
}
export class Circle {
constructor(radius) {
this.radius = radius
}
}
// 方式 2:列表导出
const PI = 3.14159
function add(a, b) {
return a + b
}
class Circle {}
export { PI, add, Circle }
// 方式 3:重命名导出
export { PI as MathPI, add as sum }
// ========== 默认导出(Default Export)==========
// 一个模块只能有一个 default
export default function multiply(a, b) {
return a * b
}
// 或者
const config = { apiUrl: '/api' }
export default config2. 导入方式
javascript
// ========== 具名导入 ==========
// 基本导入
import { PI, add } from './math.js'
// 重命名导入
import { PI as MathPI, add as sum } from './math.js'
// 导入全部
import * as MathUtils from './math.js'
console.log(MathUtils.PI)
console.log(MathUtils.add(1, 2))
// ========== 默认导入 ==========
// 可以自定义名称
import multiply from './math.js'
import config from './config.js'
// 默认 + 具名混合
import multiply, { PI, add } from './math.js'
// ========== 侧边效应导入 ==========
// 只执行模块,不导入内容
import 'polyfill.js'
import './styles.css'3. 重新导出
javascript
// 重新导出
export { PI, add } from './math.js'
// 重命名后导出
export { PI as MathPI } from './math.js'
// 默认导出转具名
export { default as MathUtils } from './math.js'
// 具名转默认
export { add as default } from './math.js'
// 全部重新导出
export * from './math.js'
export * as MathUtils from './math.js'三、CommonJS vs ES Module
1. 语法对比
javascript
// ========== CommonJS (Node.js) ==========
// math.js
const PI = 3.14159
function add(a, b) {
return a + b
}
module.exports = {
PI,
add
}
// app.js
const math = require('./math.js')
console.log(math.PI)
console.log(math.add(1, 2))
// ========== ES Module (浏览器/现代 Node) ==========
// math.js
export const PI = 3.14159
export function add(a, b) {
return a + b
}
// app.js
import { PI, add } from './math.js'
console.log(PI)
console.log(add(1, 2))2. 核心差异详解
┌──────────────────────────────────────────────────────────┐
│ CommonJS vs ES Module 详细对比 │
└──────────────────────────────────────────────────────────┘
加载机制:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
CommonJS:
┌────────────────────────────────┐
│ 运行时加载 │
│ module = require('./mod') │
│ │
│ 加载整个模块到内存 │
│ 输出值的拷贝(浅拷贝) │
└────────────────────────────────┘
ES Module:
┌────────────────────────────────┐
│ 编译时加载 (静态分析) │
│ import { count } from './mod' │
│ │
│ 输出值的引用(实时绑定) │
└────────────────────────────────┘
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
值传递对比:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
CommonJS - 值的拷贝:
// mod.js
let count = 1
setTimeout(() => count = 2, 1000)
module.exports = { count }
// main.js
const { count } = require('./mod.js')
console.log(count) // 1
// 1 秒后 count 仍然是 1
ES Module - 值的引用:
// mod.js
export let count = 1
setTimeout(() => count = 2, 1000)
// main.js
import { count } from './mod.js'
console.log(count) // 1
// 1 秒后 count 自动变为 2 ✓
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━3. 完整对比表
| 特性 | CommonJS | ES Module |
|---|---|---|
| 遵循规范 | CommonJS | ES6 |
| 加载时机 | 运行时 | 编译时 |
| 加载方式 | 动态 require() | 静态 import |
| this 指向 | 当前模块对象 | undefined |
| 输出形式 | 值的拷贝 | 值的引用 |
| 循环依赖 | 可能不完整 | 正确处理 |
| tree-shaking | ❌ 不支持 | ✓ 支持 |
| 顶层 await | ✓ 支持 | ✓ 支持 |
| 文件扩展名 | .cjs / .js | .mjs / .js |
| 主要环境 | Node.js | 浏览器/现代 Node |
四、tree-shaking 原理
1. 什么是 tree-shaking
┌──────────────────────────────────────────────────────────┐
│ tree-shaking 原理 │
└──────────────────────────────────────────────────────────┘
概念:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
tree-shaking 是一种构建优化技术
用于移除 JavaScript 中未使用的代码
前提条件:
✓ 必须使用 ES Module(静态分析)
✓ 代码必须是纯的(无副作用)
✓ 需要配合构建工具(Webpack、Rollup 等)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
工作流程:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
源代码:
// utils.js
export function used() {
return 'used'
}
export function unused() {
return 'unused'
}
// main.js
import { used, unused } from './utils.js'
console.log(used())
↓ 构建工具分析
发现 unused 从未被使用
↓ 打包时移除 dead code
最终打包结果:
// bundle.js
function used() { return 'used' }
console.log(used())
// unused 函数被摇掉!✓
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━2. tree-shaking 示例
javascript
// utils.js
export const usedFunction = () => 'I am used'
export const unusedFunction = () => 'I am unused'
export const USED_CONSTANT = 'used'
export const UNUSED_CONSTANT = 'unused'
// main.js
import { usedFunction, USED_CONSTANT } from './utils.js'
console.log(usedFunction())
console.log(USED_CONSTANT)
// 打包后(优化后):
const usedFunction = () => 'I am used'
const USED_CONSTANT = 'used'
console.log(usedFunction())
console.log(USED_CONSTANT)
// unusedFunction 和 UNUSED_CONSTANT 被移除 ✓
// 优势可视化:
优化前: ████████████████████ 10KB
优化后: ████████████ 6KB
体积减少:40% 📦3. 如何支持 tree-shaking
javascript
// ✓ 好的实践:纯函数
export function add(a, b) {
return a + b
}
export const PI = 3.14159
// ✗ 不好的实践:有副作用
export const config = (() => {
console.log('初始化配置') // 副作用
return { apiUrl: '/api' }
})()
// ✗ 修改全局状态
export function init() {
window.myApp = {} // 修改全局
}
// ✓ 更好的做法
export function createConfig() {
return { apiUrl: '/api' }
}
// 在入口文件中调用
import { createConfig } from './config.js'
const config = createConfig()五、循环依赖问题
1. CommonJS 的循环依赖
javascript
// a.js
const b = require('./b.js')
console.log('a 中的 b:', b)
module.exports = {
name: 'A',
getB: () => b
}
// b.js
const a = require('./a.js')
console.log('b 中的 a:', a)
module.exports = {
name: 'B',
getA: () => a
}
// 运行结果:
// b 中的 a: {} (空对象,因为 a 还没加载完)
// a 中的 b: { name: 'B', getA: [Function] }
// 问题:可能获取到不完整的导出2. ES Module 的循环依赖
javascript
// a.js
import b from './b.js'
export const name = 'A'
export default { name, getB: () => b }
// b.js
import a from './a.js'
export const name = 'B'
export default { name, getA: () => a }
// 运行结果:
// ES Module 能正确处理循环依赖
// 因为是在编译时建立依赖关系
// 运行时才执行代码六、实际应用
1. 模块化项目结构
src/
├── index.js # 入口文件
├── utils/
│ ├── index.js # 工具函数汇总
│ ├── string.js # 字符串工具
│ ├── array.js # 数组工具
│ └── object.js # 对象工具
├── components/
│ ├── Button/
│ │ ├── index.js # 导出组件
│ │ └── Button.vue
│ └── Input/
│ ├── index.js
│ └── Input.vue
└── api/
├── index.js # API 汇总
├── user.js # 用户相关
└── product.js # 产品相关
// utils/index.js - 统一导出
export * from './string.js'
export * from './array.js'
export * from './object.js'
// 或按需导出
export { capitalize, trim } from './string.js'
export { flatten, unique } from './array.js'2. 按需加载
javascript
// 路由懒加载
const routes = [
{
path: '/home',
component: () => import('@/views/Home.vue')
},
{
path: '/about',
component: () => import('@/views/About.vue')
}
]
// 动态 import
async function loadModule(moduleName) {
const module = await import(`./modules/${moduleName}.js`)
return module.default
}
// 条件加载
if (condition) {
const { heavyFunction } = await import('./heavy-module.js')
heavyFunction()
}3. 第三方库导入
javascript
// 导入整个库
import _ from 'lodash'
_.map([1, 2, 3], x => x * 2)
// ✓ 更好的做法:按需导入
import map from 'lodash/map'
import filter from 'lodash/filter'
// 或使用 babel-plugin-lodash 自动按需
import { map, filter } from 'lodash'
// 构建时自动转换为单独导入
// Vue 组件导入
import { defineComponent, ref, computed } from 'vue'
// React Hooks 导入
import { useState, useEffect, useCallback } from 'react'七、面试标准回答
ES6 Module 是 JavaScript 的官方模块标准,使用 export 和 import 关键字来导出和导入模块。
导出方式有两种:
- 默认导出(default export):一个模块只能有一个,导入时可以自定义名称
- 具名导出(named export):可以有多个,导入时必须使用相同的名称或重命名
与 CommonJS 的主要区别:
- 加载时机:CommonJS 是运行时加载,ES Module 是编译时加载
- 输出形式:CommonJS 输出值的拷贝,ES Module 输出值的引用(实时绑定)
- 循环依赖:CommonJS 可能获取到不完整的导出,ES Module 能正确处理
- tree-shaking:CommonJS 不支持,ES Module 支持移除未使用代码
tree-shaking 的原理是:
- 利用 ES Module 的静态分析特性
- 在构建时识别未使用的导出
- 从最终包中移除这些代码
- 可以显著减小打包体积(约 30-40%)
实际项目中,我会:
- 优先使用具名导出(便于 tree-shaking)
- 默认导出用于导出单个组件或类
- 使用统一入口文件管理导出
- 对大型库使用按需导入或动态导入
最佳实践是保持模块的纯粹性(无副作用),这样构建工具才能更好地优化。
八、记忆口诀
Module 模块化歌诀:
ES6 Module 是标准,
export import 来协作。
default 只能有一个,
named 可以有很多!
CommonJS 是老将,
require 来加载。
值拷贝不共享,
tree-shaking 不支持!
ESM 是未来,
值引用实时绑。
静态分析好处多,
构建优化靠它了!九、推荐资源
十、总结一句话
- ES Module: 静态导入 + 值的引用 = 现代化模块化 📦
- tree-shaking: 移除死码 + 减小体积 = 构建优化利器 ⚡
- vs CommonJS: 编译时 + 实时绑定 = 更优的选择 ✓